feat: tdp sticky buy swap cta ASSETS-3674 - #45593
Conversation
Pin Swap and Buy to the bottom of TDP V2 so they stay reachable while scrolling, matching the mobile sticky footer layout and safe-area handling.
Assert the TDP sticky footer stays pinned to the viewport bottom across scroll interactions.
Keep the sticky CTA assertion focused on page behavior despite the known SubscriptionsController startup race.
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
✨ Files requiring CODEOWNER review ✨👨🔧 @MetaMask/core-extension-ux (1 files, +1 -1)
🧪 @MetaMask/qa (1 files, +74 -0)
|
salimtb
left a comment
There was a problem hiding this comment.
Inline notes on the key changes to help the review.
| * @param props.isMarketClosed - Whether the asset's stock market is closed. | ||
| * @param props.isSigningEnabled - Whether the selected account can sign. | ||
| */ | ||
| export const AssetStickyActions = ({ |
There was a problem hiding this comment.
New component: the sticky Buy/Swap bar. It deliberately reuses the same navigation hooks (useRampsNavigation, useBridging), analytics, and the security-trust CTA gate (gateCtaAction) as the under-chart TokenButtons, so behavior stays identical , only the placement is new.
| openBridgeExperience( | ||
| MetaMetricsSwapsEventSource.MainView, | ||
| ALL_ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId) | ||
| ? getSwapNativeTokenWithOverridesForChain(chainId) |
There was a problem hiding this comment.
Native swaps start from the chain's default bridge asset, with per-chain overrides (e.g. Arc → ERC20 USDC). This is why getSwapNativeTokenWithOverridesForChain had to be exported from coin-buttons.tsx.
| runSwap(); | ||
| }, [asset, chainId, gateCtaAction, openBridgeExperience]); | ||
|
|
||
| const isSwapDisabled = |
There was a problem hiding this comment.
Swap is gated the same way as elsewhere: external services on, CTA gate ready, market open, and (for native assets) signing enabled. Buy is only blocked for ERC-721 / until the gate is ready.
| }; | ||
|
|
||
| function getSwapNativeTokenWithOverridesForChain(chainId: string): BridgeAsset { | ||
| export function getSwapNativeTokenWithOverridesForChain( |
There was a problem hiding this comment.
Only change here: export this helper so the new sticky bar can reuse the exact native-swap override logic instead of duplicating it. No behavior change.
| </Box> | ||
| {/* Sibling of `asset__content` so it is a direct child of the scrolling | ||
| container, which is what lets it stick to the bottom of the viewport. */} | ||
| <AssetStickyActions |
There was a problem hiding this comment.
Mounted as a sibling of asset__content (a direct child of the scrolling container) , that positioning is what lets position: sticky pin it to the bottom of the viewport.
| box-shadow: 0 -4px 12px var(--color-shadow-default); | ||
| padding: 12px 16px; | ||
| // Safe-area handling for devices with a home indicator / rounded corners. | ||
| padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); |
There was a problem hiding this comment.
Mirrors the mobile sticky footer: pinned bottom, elevated above content, and env(safe-area-inset-bottom) padding for home-indicator / rounded-corner devices.
|
|
||
| /* Lift the toaster above fixed CTA footers */ | ||
| :root:has(.cta-footer, .multichain-page-footer, .dapp-connection-control-bar, .bottom-nav-bar) { | ||
| :root:has(.cta-footer, .multichain-page-footer, .dapp-connection-control-bar, .bottom-nav-bar, .asset-page__sticky-actions) { |
There was a problem hiding this comment.
Adds the new sticky bar to the list of fixed footers the toaster lifts above, so toasts don't render underneath it.
| title: (this as Context).test?.fullTitle(), | ||
| ethConversionInUsd: 1700, | ||
| // Known SubscriptionsController startup race; unrelated to this page. | ||
| ignoredConsoleErrors: ['getSubscriptions'], |
There was a problem hiding this comment.
Filtering the getSubscriptions console error , it's a known SubscriptionsController startup race unrelated to this page, and acceptable to ignore in an e2e assertion focused on footer positioning.
| await driver.executeScript(` | ||
| const scroller = document.querySelector('.main-container.asset__container'); | ||
| if (scroller) { | ||
| scroller.scrollTo(0, scroller.scrollHeight); |
There was a problem hiding this comment.
Scrolls the real scrollport (.main-container.asset__container) to the bottom, then re-asserts the bar is still pinned — a non-sticky footer would have scrolled out of view here.
| if (!bar) { | ||
| return false; | ||
| } | ||
| const rect = bar.getBoundingClientRect(); |
There was a problem hiding this comment.
Pinning check compares the bar's rect.bottom to window.innerHeight with an 8px tolerance so scrollbar / safe-area padding don't cause flakes.
Builds ready [f00f9a5] [reused from 840b0a6]
⚡ Performance Benchmarks (Total: 🟢 12 pass · 🟡 4 warn · 🔴 3 fail)
Bundle size diffs [🚀 Bundle size reduced!]
|
Flaky E2E / extension good-practices reviewVerdict: Mostly follows extension flaky-e2e practices, with a couple of clear POM gaps. What it gets right
Gaps vs extension e2e / flakiness guidance
SummarySolid on fixtures, mocks, waits, testids, and layering. Not fully clean on POM: the scroll/CSS interaction in the spec is the main miss. Fix that (and track |
racitores
left a comment
There was a problem hiding this comment.
Just some comments in #45593 (comment)
Address review feedback: the scroll interaction lived in the spec and targeted a brittle CSS class. Add a `data-testid` to the TDP scroll container and a `scrollToBottom()` method on the AssetStickyActions page object so the spec drives UI actions through the POM and a stable testid. Co-authored-by: Cursor <cursoragent@cursor.com>
acdd871
Address review feedback: the SubscriptionsController startup race that the `ignoredConsoleErrors` entry hides is now tracked in #45612 rather than explained by a code comment alone. Co-authored-by: Cursor <cursoragent@cursor.com>
Builds ready [05d4325]
⚡ Performance Benchmarks (Total: 🟢 13 pass · 🟡 8 warn · 🔴 3 fail)
Bundle size diffs [🚀 Bundle size reduced!]
|
|
|
||
| /* Lift the toaster above fixed CTA footers */ | ||
| :root:has(.cta-footer, .multichain-page-footer, .dapp-connection-control-bar, .bottom-nav-bar) { | ||
| :root:has(.cta-footer, .multichain-page-footer, .dapp-connection-control-bar, .bottom-nav-bar, .asset-page__sticky-actions) { |
There was a problem hiding this comment.
@salimtb can we use .cta-footer? It's primary used for toast avoidance
There was a problem hiding this comment.
sure , let me do it now
| // Persistent bottom CTA bar (Buy / Swap) for the Token Detail Page V2. Mirrors | ||
| // the Mobile sticky footer: pinned to the bottom of the scroll area, elevated | ||
| // above the content, and padded for the device safe-area inset. | ||
| .asset-page__sticky-actions { |
There was a problem hiding this comment.
Can you try moving these to tailwind? Trying to avoid adding more Sass
Builds ready [05d4325]
⚡ Performance Benchmarks (Total: 🟢 13 pass · 🟡 8 warn · 🔴 3 fail)
Bundle size diffs [🚀 Bundle size reduced!]
|
Builds ready [05d4325]
⚡ Performance Benchmarks (Total: 🟢 13 pass · 🟡 8 warn · 🔴 3 fail)
Bundle size diffs [🚀 Bundle size reduced!]
|
|
Builds ready [196e5d1]
⚡ Performance Benchmarks (Total: 🟢 14 pass · 🟡 7 warn · 🔴 3 fail)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|



Description
Adds a persistent bottom CTA bar for Buy and Swap on the Extension Token Detail Page (TDP) V2.
Reason: Users scrolling the token detail page could not easily reach Buy and Swap without scrolling back up. Mobile already has a sticky footer for these actions, and Extension should match that behavior.
Solution: Introduce an
AssetStickyActionsfooter pinned to the bottom of the TDP scroll container. It keeps Buy and Swap reachable while scrolling, follows mobile sticky-footer layout conventions (including safe-area padding), and uses the same Buy/Swap navigation wiring and security-trust CTA gating as the existing under-chart actions. The original under-chart buttons remain unchanged.Changelog
CHANGELOG entry: Added a sticky Buy and Swap action bar to the token detail page
Related issues
Fixes:
Manual testing steps
yarn start), then unlock/create a wallet.Screenshots/Recordings
Before
No sticky footer; Buy and Swap were only available in the action row under the chart.
After
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Medium Risk
Touches buy/swap navigation and security-trust gating on a high-traffic page, but behavior mirrors existing CTAs with focused layout changes rather than new payment or auth logic.
Overview
Adds a persistent bottom Buy / Swap bar on Token Detail Page V2 so those actions stay reachable while scrolling, aligned with mobile sticky-footer behavior. The new
AssetStickyActionscomponent is a direct sibling of the page content inside the scroll container and reuses ramps buy navigation, bridge/swap flows, security-trust CTA gating, and the same disable rules (market closed, signing, external services) as existing actions; under-chart controls are unchanged.Layout/CSS:
min-height: 0on the asset container and its wrapper fixes flex scrolling soposition: stickypins the bar to the viewport bottom; styles include safe-area padding and elevation. Toasts lift above.asset-page__sticky-actionsvia an updated:root:has(...)rule. Scroll containers getdata-testid="asset-page-scroll-container"for e2e.Tests: Unit tests cover buy/swap routing, ramps gate analytics, and disabled swap when the market is closed; e2e asserts the bar stays pinned after scrolling.
getSwapNativeTokenWithOverridesForChainis exported fromcoin-buttonsfor native swap overrides in the sticky bar.Reviewed by Cursor Bugbot for commit 05d4325. Bugbot is set up for automated code reviews on this repo. Configure here.